package com.lorizz.docrepo.repository.impl;

import java.security.Principal;
import java.util.ArrayList;
import java.util.List;

import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.security.AccessControlEntry;
import javax.jcr.security.AccessControlList;
import javax.jcr.security.AccessControlPolicy;
import javax.jcr.security.AccessControlPolicyIterator;
import javax.jcr.security.Privilege;

import com.lorizz.docrepo.DocrepoException;
import com.lorizz.docrepo.model.Document;
import com.lorizz.docrepo.model.Folder;
import com.lorizz.docrepo.model.Permission;
import com.lorizz.docrepo.model.impl.DocumentImpl;
import com.lorizz.docrepo.model.impl.FolderImpl;
import com.lorizz.docrepo.repository.DocumentRepository;
import com.lorizz.docrepo.security.RepositoryPrincipal;

public class JcrDocumentRepository implements DocumentRepository {

    private Session session;

    public JcrDocumentRepository(Session session) {
        this.session = session;
    }

    public Session getSession() {
        return this.session;
    }

    @Override
    public Folder getFolder(String absPath) {
        try {
            return new FolderImpl(session.getNode(absPath));
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    @Override
    public Document getDocument(String uuid) {
        try {
            return new DocumentImpl(session.getNodeByIdentifier(uuid));
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    @Override
    public Folder createFolder(String name) {
        return new FolderImpl(session, name);
    }

    @Override
    public void save() {
        try {
            session.save();
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    @Override
    public void refresh(boolean keepChanges) {
        try {
            session.refresh(keepChanges);
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    @Override
    public void disconnect() {
        session.logout();
    }

    @Override
    public Folder[] getFolders() {
        try {
            List<Folder> l = new ArrayList<Folder>();
            NodeIterator i = session.getRootNode().getNodes();
            while (i.hasNext()) {
                Node n = i.nextNode();
                if (JcrDocumentRepositoryConstants.NT_FOLDER.equals(n
                        .getPrimaryNodeType().getName())) {
                    l.add(new FolderImpl(n));
                }
            }
            return l.toArray(new Folder[] {});
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    private boolean checkPermission(String absPath, String... privileges) {
        try {
            Privilege[] ps = new Privilege[privileges.length];
            int pos = 0;
            for (String p : privileges) {
                ps[pos] = session.getAccessControlManager()
                        .privilegeFromName(p);
                pos++;
            }
            return session.getAccessControlManager().hasPrivileges(absPath, ps);
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    @Override
    public boolean canRead(String absPath) {
        return checkPermission(absPath, Privilege.JCR_READ);
    }

    @Override
    public boolean canWrite(Folder folder) {
        return checkPermission(folder.getFullPath(),
                Privilege.JCR_ADD_CHILD_NODES, Privilege.JCR_MODIFY_PROPERTIES,
                Privilege.JCR_REMOVE_CHILD_NODES,
                Privilege.JCR_NODE_TYPE_MANAGEMENT,
                Privilege.JCR_VERSION_MANAGEMENT);
    }

    @Override
    public boolean canModifyPermissions(Folder folder) {
        return checkPermission(folder.getFullPath(),
                Privilege.JCR_READ_ACCESS_CONTROL,
                Privilege.JCR_MODIFY_ACCESS_CONTROL);
    }

    @Override
    public void assignPermissions(String absPath, String principal,
            Permission... permissions) {
        Principal p = new RepositoryPrincipal(principal);
        AccessControlList acl = findACL(absPath);
        AccessControlEntry ace = findACE(acl, p);
        if (ace == null) {
            if (permissions == null || permissions.length == 0)
                return;
            try {
                acl.addAccessControlEntry(p, getJcrPrivileges(permissions));
                session.getAccessControlManager().setPolicy(absPath, acl);
            } catch (RepositoryException e) {
                throw new DocrepoException(e);
            }
        } else {
            try {
                acl.removeAccessControlEntry(ace);
                if (permissions != null && permissions.length > 0) {
                    acl.addAccessControlEntry(p, getJcrPrivileges(permissions));
                }
                session.getAccessControlManager().setPolicy(absPath, acl);
            } catch (RepositoryException e) {
                throw new DocrepoException(e);
            }
        }
    }

    private Privilege[] getJcrPrivileges(Permission[] permissions) {
        try {
            List<Privilege> ps = new ArrayList<Privilege>();
            for (Permission p : permissions) {
                switch (p) {
                case READ:
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_READ));
                    break;
                case WRITE:
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_ADD_CHILD_NODES));
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_MODIFY_PROPERTIES));
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_REMOVE_CHILD_NODES));
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_NODE_TYPE_MANAGEMENT));
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_VERSION_MANAGEMENT));
                    break;
                case MODIFY_PERMISSIONS:
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_READ_ACCESS_CONTROL));
                    ps.add(session.getAccessControlManager().privilegeFromName(
                            Privilege.JCR_MODIFY_ACCESS_CONTROL));
                    break;
                }
            }
            return ps.toArray(new Privilege[] {});
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    private AccessControlList findACL(String absPath) {
        try {
            if ("/".equals(absPath)) {
                AccessControlPolicy[] acps = session.getAccessControlManager()
                        .getPolicies(absPath);

                for (AccessControlPolicy acp : acps) {
                    if (acp instanceof AccessControlList) {
                        return (AccessControlList) acp;
                    }
                }
            } else {
                AccessControlPolicyIterator acpi = session
                        .getAccessControlManager().getApplicablePolicies(
                                absPath);
                while (acpi.hasNext()) {
                    AccessControlPolicy acp = acpi.nextAccessControlPolicy();
                    if (acp instanceof AccessControlList) {
                        return (AccessControlList) acp;
                    }
                }
            }
            throw new DocrepoException("No AccessControlList defined in ["
                    + absPath + "].");
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

    private AccessControlEntry findACE(AccessControlList acl,
            Principal principal) {
        try {
            for (AccessControlEntry ace : acl.getAccessControlEntries()) {
                if (ace.getPrincipal().equals(principal))
                    return ace;
            }
            return null;
        } catch (RepositoryException e) {
            throw new DocrepoException(e);
        }
    }

}
